/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/

"use strict";

const util = require("util");
const webpackOptionsSchemaCheck = require("../schemas/WebpackOptions.check.js");
const webpackOptionsSchema = require("../schemas/WebpackOptions.json");
const Compiler = require("./Compiler");
const MultiCompiler = require("./MultiCompiler");
const WebpackOptionsApply = require("./WebpackOptionsApply");
const {
    applyWebpackOptionsDefaults,
    applyWebpackOptionsBaseDefaults
} = require("./config/defaults");
const { getNormalizedWebpackOptions } = require("./config/normalization");
const NodeEnvironmentPlugin = require("./node/NodeEnvironmentPlugin");
const memoize = require("./util/memoize");

/** @typedef {import("../declarations/WebpackOptions").WebpackOptions} WebpackOptions */
/** @typedef {import("./Compiler").WatchOptions} WatchOptions */
/** @typedef {import("./MultiCompiler").MultiCompilerOptions} MultiCompilerOptions */
/** @typedef {import("./MultiStats")} MultiStats */
/** @typedef {import("./Stats")} Stats */

const getValidateSchema = memoize(() => require("./validateSchema"));

/**
 * @template T
 * @callback Callback
 * @param {Error=} err
 * @param {T=} stats
 * @returns {void}
 */

/**
 * @param {WebpackOptions[]} childOptions options array
 * @param {MultiCompilerOptions} options options
 * @returns {MultiCompiler} a multi-compiler
 */
const createMultiCompiler = (childOptions, options) => {
    const compilers = childOptions.map(options => createCompiler(options));
    const compiler = new MultiCompiler(compilers, options);
    for (const childCompiler of compilers) {
        if (childCompiler.options.dependencies) {
            compiler.setDependencies(
                childCompiler,
                childCompiler.options.dependencies
            );
        }
    }
    return compiler;
};

/**
 * @param {WebpackOptions} rawOptions options object
 * @returns {Compiler} a compiler
 */
const createCompiler = rawOptions => {
    const options = getNormalizedWebpackOptions(rawOptions);
    applyWebpackOptionsBaseDefaults(options);
    const compiler = new Compiler(options.context);
    compiler.options = options;
    new NodeEnvironmentPlugin({
        infrastructureLogging: options.infrastructureLogging
    }).apply(compiler);
    if (Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
            if (typeof plugin === "function") {
                plugin.call(compiler, compiler);
            } else {
                plugin.apply(compiler);
            }
        }
    }
    applyWebpackOptionsDefaults(options);
    compiler.hooks.environment.call();
    compiler.hooks.afterEnvironment.call();
    new WebpackOptionsApply().process(options, compiler);
    compiler.hooks.initialize.call();
    return compiler;
};

/**
 * @callback WebpackFunctionSingle
 * @param {WebpackOptions} options options object
 * @param {Callback<Stats>=} callback callback
 * @returns {Compiler} the compiler object
 */

/**
 * @callback WebpackFunctionMulti
 * @param {WebpackOptions[] & MultiCompilerOptions} options options objects
 * @param {Callback<MultiStats>=} callback callback
 * @returns {MultiCompiler} the multi compiler object
 */

const webpack = /** @type {WebpackFunctionSingle & WebpackFunctionMulti} */ (
    /**
     * @param {WebpackOptions | (WebpackOptions[] & MultiCompilerOptions)} options options
     * @param {Callback<Stats> & Callback<MultiStats>=} callback callback
     * @returns {Compiler | MultiCompiler}
     */
    (options, callback) => {
        const create = () => {
            if (!webpackOptionsSchemaCheck(options)) {
                getValidateSchema()(webpackOptionsSchema, options);
            }
            /** @type {MultiCompiler|Compiler} */
            let compiler;
            let watch = false;
            /** @type {WatchOptions|WatchOptions[]} */
            let watchOptions;
            if (Array.isArray(options)) {
                /** @type {MultiCompiler} */
                compiler = createMultiCompiler(options, options);
                watch = options.some(options => options.watch);
                watchOptions = options.map(options => options.watchOptions || {});
            } else {
                /** @type {Compiler} */
                compiler = createCompiler(options);
                watch = options.watch;
                watchOptions = options.watchOptions || {};
            }
            return { compiler, watch, watchOptions };
        };
        if (callback) {
            try {
                const { compiler, watch, watchOptions } = create();
                if (watch) {
                    compiler.watch(watchOptions, callback);
                } else {
                    compiler.run((err, stats) => {
                        compiler.close(err2 => {
                            callback(err || err2, stats);
                        });
                    });
                }
                return compiler;
            } catch (err) {
                process.nextTick(() => callback(err));
                return null;
            }
        } else {
            const { compiler, watch } = create();
            if (watch) {
                util.deprecate(
                    () => {},
                    "A 'callback' argument need to be provided to the 'webpack(options, callback)' function when the 'watch' option is set. There is no way to handle the 'watch' option without a callback.",
                    "DEP_WEBPACK_WATCH_WITHOUT_CALLBACK"
                )();
            }
            return compiler;
        }
    }
);

module.exports = webpack;